package drds.common.db.xml_mapping.script.xml_tags;

import drds.common.db.xml_mapping.parsing.GenericTokenParser;
import drds.common.db.xml_mapping.session.Configuration;

import java.util.Map;


public class ForEach implements DynamicNode {
  public static final String ITEM_PREFIX = "__frch_";

  private final ExpressionEvaluator evaluator;
  private final String collectionExpression;
  private final DynamicNode contents;
  private final String open;
  private final String close;
  private final String separator;
  private final String item;
  private final String index;
  private final Configuration configuration;

  public ForEach(Configuration configuration, DynamicNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }

  private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
  }

  @Override
  public boolean apply(DynamicNodeContext dynamicNodeContext) {
    Map<String, Object> bindings = dynamicNodeContext.getBindings();
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    applyOpen(dynamicNodeContext);
    int i = 0;
    for (Object o : iterable) {
      DynamicNodeContext oldContext = dynamicNodeContext;
      if (first || separator == null) {
        dynamicNodeContext = new PrefixedNodeContext(dynamicNodeContext, "");
      } else {
        dynamicNodeContext = new PrefixedNodeContext(dynamicNodeContext, separator);
      }
      int uniqueNumber = dynamicNodeContext.getUniqueNumber();
      // Issue #709
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(dynamicNodeContext, mapEntry.getKey(), uniqueNumber);
        applyItem(dynamicNodeContext, mapEntry.getValue(), uniqueNumber);
      } else {
        applyIndex(dynamicNodeContext, i, uniqueNumber);
        applyItem(dynamicNodeContext, o, uniqueNumber);
      }
      contents.apply(new FilteredDynamicNodeContext(configuration, dynamicNodeContext, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedNodeContext) dynamicNodeContext).isPrefixApplied();
      }
      dynamicNodeContext = oldContext;
      i++;
    }
    applyClose(dynamicNodeContext);
    dynamicNodeContext.getBindings().remove(item);
    dynamicNodeContext.getBindings().remove(index);
    return true;
  }

  private void applyIndex(DynamicNodeContext context, Object o, int i) {
    if (index != null) {
      context.bind(index, o);
      context.bind(itemizeItem(index, i), o);
    }
  }

  private void applyItem(DynamicNodeContext context, Object o, int i) {
    if (item != null) {
      context.bind(item, o);
      context.bind(itemizeItem(item, i), o);
    }
  }

  private void applyOpen(DynamicNodeContext dynamicNodeContext) {
    if (open != null) {
      dynamicNodeContext.appendSql(open);
    }
  }

  private void applyClose(DynamicNodeContext dynamicNodeContext) {
    if (close != null) {
      dynamicNodeContext.appendSql(close);
    }
  }

  private static class FilteredDynamicNodeContext extends DynamicNodeContext {
    private final DynamicNodeContext delegate;
    private final int index;
    private final String itemIndex;
    private final String item;

    public FilteredDynamicNodeContext(Configuration configuration, DynamicNodeContext delegate, String itemIndex, String item, int i) {
      super(configuration, null);
      this.delegate = delegate;
      this.index = i;
      this.itemIndex = itemIndex;
      this.item = item;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    @Override
    public void appendSql(String sql) {
      GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", content -> {
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
          newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
      });

      delegate.appendSql(genericTokenParser.parse(sql));
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }

  }


  private class PrefixedNodeContext extends DynamicNodeContext {
    private final DynamicNodeContext delegate;
    private final String prefix;
    private boolean prefixApplied;

    public PrefixedNodeContext(DynamicNodeContext delegate, String prefix) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefix = prefix;
      this.prefixApplied = false;
    }

    public boolean isPrefixApplied() {
      return prefixApplied;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public void appendSql(String sql) {
      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        delegate.appendSql(prefix);
        prefixApplied = true;
      }
      delegate.appendSql(sql);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
  }

}
